package service.impl;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import models.Loan;
import models.Loan.Status;
import models.Pay;
import prefix.platform.Constants;
import prefix.platform.loan.AmortizationTable;
import prefix.util.DateUtil;
import prefix.util.OverallUtil;
import processor.LoanProcessor;
import repository.LoanRepository;
import repository.PayRepository;
import service.LoanService;
import service.PayService;
import service.exception.LoanException;
import service.exception.LoanException.CODE;
import service.exception.PayException;

public class LoanServiceImpl extends AbstractServiceImpl implements LoanService {

    protected Loan dbloan;

    public LoanServiceImpl() {
        this.dbloan = new Loan();
    }

    public LoanServiceImpl(Loan loan) {
        this.dbloan = loan;
    }

    @Override
    public LoanProcessor getProcessor() {
        // TODO Auto-generated method stub
        return new LoanProcessor(this.dbloan);
    }

    public boolean validateFunding() {
        if (this.dbloan.getAmount() != this.getCommitAmount())
            return false;
        if (this.dbloan.getAmount() != this.dbloan.getAppAmount())
            return false;
        return true;
    }

    public double getCommitAmount() {
        return LoanRepository.getCommitAmount(this.dbloan.getId());
    }

    public static double getAcceptedOriginationFee(double amount, double rate) {
        double origFee = amount * rate;
        return OverallUtil.round2Decimal(origFee).doubleValue();
    }

    public double getAmountToBeCurrent() {
        return 0;
    }

    public double getAmountToBeFullyPaid() {
        return 10000;
    }

    public Pay getNextPay(Pay existPay) {
        Pay pay = null;

        if (this.dbloan.getCurrentPayId() == null
                && OverallUtil.isIn(this.dbloan.getStatus(), Loan.Status.ISSUING, Loan.Status.REVIEW_FINAL,
                        Loan.Status.INFUNDING))
            existPay = this.createPreFirstPay();

        pay = createNextPay(existPay);

        return pay;
    }

    public Loan.Status getLoanStatusAfterPaymentOld() throws PayException {
        // only get the loan status based on the current pay status
        Pay currentPay = this.dbloan.getCurrentPay();
        Pay.Status payStatus = currentPay.getStatus();
        double prncpUnpaid = currentPay.getPrncpAccr() - currentPay.getPrncpPaid();
        double newOutstandingPrincipal = currentPay.getPrncpOut() - currentPay.getPrncpPaid();

        // Paid and still open or complete
        if (prncpUnpaid <= Constants.PRINCIPAL_BALANCE_PENALTY_5CENT) {
            if (newOutstandingPrincipal <= Constants.PRINCIPAL_BALANCE_PENALTY_5CENT)
                return Loan.Status.COMPLETED;
            switch (payStatus) {
            case CLOSE_PAID:
            case CURRENT:
                return Loan.Status.CURRENT;
            default:
                throw new PayException(PayException.CODE.INVALID_PAY_STATUS);
            }
        }

        if (payStatus.equals(Pay.Status.CHARGE_OFF))
            return Loan.Status.DEFAULTED;
        List<Pay> payList = this.dbloan.getPayList();
        int numberOfConsecutiveFailed = 0;
        for (Pay pay : payList) {
            if (pay.getReceivedAmount() < pay.getDueAmount())
                numberOfConsecutiveFailed++;
        }
        if (numberOfConsecutiveFailed >= 3)
            return Loan.Status.DEFAULTED;

        switch (payStatus) {
        case GRACE_PERIOD:
            return Loan.Status.GRACE_CURRENT;
        case LATE_30_90:
        case LATE_7_30:
            return Loan.Status.DELINQUENT;
        case LATE_90:
            return Loan.Status.DEFAULTED;
        case CURRENT:
            return Loan.Status.CURRENT;
        default:
            throw new PayException(PayException.CODE.INVALID_PAY_STATUS);
        }
    }

    public Status getLoanStatusAfterPayment() throws LoanException, PayException {

        List<Pay> payList = PayRepository.getProcessedPay(this.dbloan.getId());
        if (payList == null || payList.size() <= 0)
            throw new LoanException(CODE.NO_PAYLIST);
        Pay targetPay = null;
        for (Pay pay : payList) {
            if (pay.getReceivedAmount() < pay.getDueAmount())
                targetPay = pay;
        }
        if (targetPay == null) {
            targetPay = payList.get(payList.size() - 1);
        }
        double prncpUnpaid = targetPay.getPrncpAccr() - targetPay.getPrncpPaid();
        double newOutstandingPrincipal = targetPay.getPrncpOut() - targetPay.getPrncpPaid();
        // Paid and still open or complete
        if (prncpUnpaid <= Constants.PRINCIPAL_BALANCE_PENALTY_5CENT) {
            if (newOutstandingPrincipal <= Constants.PRINCIPAL_BALANCE_PENALTY_5CENT)
                return Loan.Status.COMPLETED;
            switch (targetPay.getStatus()) {
            case CLOSE_PAID:
            case CURRENT:
                return Loan.Status.CURRENT;
            default:
                throw new PayException(PayException.CODE.INVALID_PAY_STATUS);
            }
        }
        switch (targetPay.getStatus()) {
        case GRACE_PERIOD:
            return Loan.Status.GRACE_CURRENT;
        case CURRENT:
            return Loan.Status.CURRENT;
        case LATE_30_90:
        case LATE_7_30:
            return Loan.Status.DELINQUENT;
        case LATE_90:
        case CHARGE_OFF:
            return Loan.Status.DEFAULTED;
        default:
            throw new PayException(PayException.CODE.INVALID_PAY_STATUS);
        }

    }

    public Pay createNextPay(Pay existPay) {

        Pay futurePay = Pay.createEmptyPay(null);
        double RPA = this.getRoundedMtlPayment();
        double outstandingPrincipal = existPay.getPrncpOut() - existPay.getPrncpAccr();
        futurePay.setPrncpOut(outstandingPrincipal);
        double intAccr = outstandingPrincipal * this.dbloan.getIntRate() / 12;
        futurePay.setIntAccr(intAccr);
        futurePay.setIntPaid(0);

        double commitAmount = this.getCommitAmount();
        double brMgmtAccrued = commitAmount * this.dbloan.getManagementFee();
        brMgmtAccrued = Math.max(brMgmtAccrued, 0.01);
        brMgmtAccrued = OverallUtil.round2Decimal(brMgmtAccrued, RoundingMode.HALF_EVEN).doubleValue();
        futurePay.setManagementFeeAccr(brMgmtAccrued);
        futurePay.setManagementFeePaid(0);

        double prncpAccrued = RPA - intAccr;
        prncpAccrued = prncpAccrued + this.lastPaymentPrncp();
        prncpAccrued = Math.min(prncpAccrued, outstandingPrincipal);
        futurePay.setPrncpAccr(prncpAccrued);
        futurePay.setPrncpPaid(0);
        futurePay.setLoanId(existPay.getLoanId());
        PayService future = new PayServiceImpl(futurePay);
        futurePay.setDueAmount(future.getAmountToBeCurrent());
        if (existPay.getDueD() != null) {
            Date dueDate = DateUtil.addMonths(existPay.getDueD(), 1);
            futurePay.setDueD(dueDate);
            futurePay.setPullD(dueDate);
        }
        return futurePay;
    }

    private double lastPaymentPrncp() {
        Date loanIssueDate = this.dbloan.getStartD();
        if (loanIssueDate == null) {
            return 0;
        }

        loanIssueDate = DateUtil.normalizeMonth(loanIssueDate);
        Date now = DateUtil.normalizeMonth(new Date());
        long differ = DateUtil.monthsBetween(loanIssueDate, now);
        if (differ == this.dbloan.getDuration())
            return this.dbloan.getAmount();
        return 0;
    }

    private Pay createPreFirstPay() {
        Pay first = Pay.createEmptyPay(null);
        first.setDueD(null);
        double outstandingPrincipal = this.dbloan.getAmount();
        double RPA = this.getRoundedMtlPayment();
        double intAccr = outstandingPrincipal * this.dbloan.getIntRate() / 12;
        double prncpAccrued = RPA - intAccr;
        first.setPrncpOut(this.dbloan.getAmount() + prncpAccrued);
        first.setPrncpPaid(prncpAccrued);
        first.setPrncpAccr(prncpAccrued);
        return first;
    }

    /**
     * Returns the monthly payment
     */
    public double getRoundedMtlPayment() {
        double mtl = this.dbloan.getRpa();
        BigDecimal roundedAmt = OverallUtil.round2Decimal(mtl, RoundingMode.CEILING);
        return roundedAmt.doubleValue();
    }

    // -------------------------------------------------------------------------------------------------------
    public double getMonthlyPayment() {
        double mtlPayment = 0;
        if (this.hasBeenIssued()) {
            mtlPayment = this.getRoundedMtlPayment();
        } else
            mtlPayment = AmortizationTable.getMonthlyPayment(this.dbloan.getAmount(), this.dbloan.getDuration(),
                    this.dbloan.getIntRate(), this.dbloan.getLoanApp().getPaymentMode().getPaymentFrequency());
        mtlPayment = OverallUtil.round2Decimal(mtlPayment, RoundingMode.CEILING).doubleValue();
        return mtlPayment;
    }

    public boolean hasBeenIssued() {
        return LoanServiceImpl.hasBeenIssued(this.dbloan.getStatus());
    }

    // ---------------------------------------------------------------------------------------------------------------
    public static boolean hasBeenIssued(Loan.Status status) {
        // TLLoan.Status status = this.getStatus();

        if (status == null)
            return false;

        switch (status) {
        case NEW:
        case INFUNDING:
        case RELEASED_EXPIRED:
        case RELEASED_REJECTED:
        case REVIEW_LISTING:
        case REVIEW_EXPIRED:
        case RELEASED_FRAUD:
        case RELEASED_UCANCEL:
        case REVIEW_FINAL:
            return false;

        case ISSUING:
        case ISSUED:
        case COMPLETED:
        case CURRENT:
        case GRACE_CURRENT:
        case DELINQUENT:
        case INCOLLECTION:
        case DEFAULTED:
        case CHARGEDOFF:
        case SETTLED:
        case SOLD:
            return true;
        case ANY:
        default:
            throw new RuntimeException("Invalid loan state");
        }

    }

    public boolean isValidLoanStateForProcess() {
        if (!OverallUtil.isIn(this.dbloan.getStatus(), Status.ISSUED, Status.CURRENT, Status.GRACE_CURRENT,
                Status.DELINQUENT, Status.INCOLLECTION, Status.DEFAULTED))
            return false;
        return true;
    }

    public Loan getDbloan() {
        return dbloan;
    }

    public void setDbloan(Loan dbloan) {
        this.dbloan = dbloan;
    }

}
